#ifndef VENUS_GRAPHICS_CAMERA_H
#define VENUS_GRAPHICS_CAMERA_H

#include <iostream>
#include "geometry/point.h"
#include "opengl.h"

namespace venus {	
class Motion {
public:
  Motion(const Point &pos, const Point &target, const Vector3 &up)
      : position_(pos), target_(target), up_(up), velocity_(1), 
        rotation_speed_(10) {  this->update_(); }
  virtual ~Motion() = 0;
  virtual void Yaw(float angle) = 0; // rotate on up vector
  virtual void Pitch(float angle) = 0; // rotate on right vector
  virtual void Roll(float angle) = 0; // rotate on look vector
  virtual void Strafe(float units) = 0; // move left/right
  virtual void Fly(float units) = 0; // move up/down
  virtual void Walk(float units) = 0; // move forward/backward
  const Point &position() const { return position_; }
  const Point &target() const { return target_; }
  const Vector3 &up() const { return up_; }
  void SetPosition(float x, float y, float z) {  SetPosition(Point(x, y, z)); }
  void SetTarget(float x, float y, float z) { SetTarget(Point(x, y, z)); }
  void SetUp(float x, float y, float z) { SetUp(Vector3(x, y, z)); }
  void SetPosition(const Point &p) { set_position(p); update_(); }
  void SetTarget(const Point &p) {  set_target(p); update_(); }
  void SetUp(const Vector3 &v) { 
    this->set_up(Normalize(v));
    this->set_right(Cross(this->look(), this->up()));
  }
  const Vector3 &velocity() const { return velocity_; }
  void set_velocity(float x, float y, float z) { 
    velocity_.x = x; velocity_.y = y; velocity_.z = z;
  }
  void inc_velocity(float x, float y, float z) {
    velocity_.x += x; velocity_.y += y; velocity_.z += z;
  }
  void set_rotation_speed(float f) { rotation_speed_ = f; }
  void inc_rotation_speed(float f) { rotation_speed_ += f; }
protected:
  /* the following three methods simply rotates the camera, and updates
   * direction information, an update of other information is needed to keep 
   * the camera work. and this update is expected to be done by derived 
   * classes according to their need
   */
  void yaw_(float angle);
  void roll_(float angle);
  void pitch_(float angle);
  /* the following three methods simply moves the camera,
   * an update of other information is needed to keep the camera work
   * and this update is for the derived class to do accordingly
   */
  void walk_(float units);
  void fly_(float units);
  void strafe_(float units);
  void set_position(const Point &p) { position_ = p; }
  void set_target(const Point &p) { target_ = p; }
  void set_up(const Vector3 &v) { up_ = v; }
  void set_right(const Vector3 &v) { right_ = v; }
  const Vector3 &right() const { return right_; }
  const Vector3 &look() const { return look_; }
  void set_look(const Vector3 &v) { look_ = v; }
private:
  void update_() {
    look_ = Normalize(target_ - position_);
    right_ = Cross(look_, up_);
  }
private:
  float rotation_speed_;
  Vector3 velocity_;
  Vector3 right_, up_, look_;
  Point position_, target_;
};

class FirstPersonMotion : public Motion {
public:
  virtual void Yaw(float angle);
  virtual void Pitch(float angle);
  virtual void Roll(float angle);
  virtual void Strafe(float units);
  virtual void Fly(float units);
  virtual void Walk(float units);
};

class SpectatorMotion : public Motion {
public:
  virtual void Strafe(float units);
  virtual void Fly(float units);
  virtual void Walk(float units);
};

class FlyMotion : public Motion {
public:
  FlyMotion(const Point &pos,  const Point &target, 
            const Vector3 &up = Vector3(0, 1, 0))
      : Motion(pos, target, up) { }
  virtual void Yaw(float angle);
  virtual void Pitch(float angle);
  virtual void Roll(float angle);
  virtual void Strafe(float units);
  virtual void Fly(float units);
  virtual void Walk(float units);
private:
  void update_(void);
};

class OrbitMotion : public Motion {
public:
  OrbitMotion(const Point &pos, const Point &target = Point(0, 0, 0), 
              const Vector3 &up = Vector3(0, 1, 0))
      : Motion(pos, target, up), min_dist_(0), max_dist_(FLT_MAX) { }
  virtual void Yaw(float angle);
  virtual void Pitch(float angle);
  virtual void Roll(float angle);
  virtual void Walk(float units);
  virtual void Strafe(float units) { }
  virtual void Fly(float units) { }
  float min_dist() const { return min_dist_; }
  float max_dist() const { return max_dist_; }
  void set_min_dist(float f) { min_dist_ = f; }
  void set_max_dist(float f) { max_dist_ = f; }
private:
  void UpdataPosition();
private:
  float min_dist_, max_dist_;
};

class Camera {

public:
  virtual float *GetProjectionMatrixf() const = 0;
  virtual float *GetViewMatrixf() const = 0;
  virtual double *GetProjectionMatrixd() const = 0;
  virtual double *GetViewMatrixd() const = 0;
};

template<typename MotionType>
class PerspectiveCameraGL : public MotionType {
public:
  PerspectiveCameraGL(const Point &pos = Point(1, 1, -1), 
                      const Point &target = Point(0, 0, 0), 
                      const Vector3 &up = Vector3(0, 1, 0)) 
      : MotionType(pos, target, up), fovy_(60.0), aspect_ratio_(1.0), 
        z_near_(0.1), z_far_(10.0) { }
  void ApplyProjectionTransform() const;
  void ApplyViewTransform() const;
  void set_fovy(double d) { fovy_ = d; }
  void set_aspect_ratio(double d) { aspect_ratio_ = d; }
  void set_z_near(double d) {z_near_ = d; }
  void set_z_far(double d) { z_far_ = d; }
  double fovy() const { return fovy_; }
  double aspect_ratio() const { return aspect_ratio_; }
  double z_near() const { return z_near_; }
  double z_far() const { return z_far_; }
  void inc_z_far(double d) { z_far_ += d; }
  void inc_z_near(double d) { z_near_ += d; }
  void inc_fovy(double d) { fovy_ +=d; }
private:
  double fovy_, aspect_ratio_, z_near_, z_far_;
};

template<typename MotionType>
inline void PerspectiveCameraGL<MotionType>::ApplyViewTransform() const { 
  const Point &eye_pos = this->position();
  const Point &target = this->target();
  const Vector3 &up = this->up();
  gluLookAt(eye_pos.x, eye_pos.y, eye_pos.z, target.x, target.y, target.z,
            up.x, up.y, up.z);
}

template<typename MotionType>
inline void PerspectiveCameraGL<MotionType>::ApplyProjectionTransform() const { 
  gluPerspective(fovy_, aspect_ratio_, z_near_, z_far_);
}

typedef PerspectiveCameraGL<OrbitMotion> OrbitPerspectiveCameraGL;

} // namespace venus

#endif//VENUS_GRAPHICS_CAMERA_H
